/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import "vs/css!./media/actions"

import { localize, localize2 } from "vs/nls"
import { IKeybindingService } from "vs/platform/keybinding/common/keybinding"
import { DomEmitter } from "vs/base/browser/event"
import { Color } from "vs/base/common/color"
import { Emitter, Event } from "vs/base/common/event"
import {
  IDisposable,
  toDisposable,
  dispose,
  DisposableStore,
  setDisposableTracker,
  DisposableTracker,
  DisposableInfo,
} from "vs/base/common/lifecycle"
import {
  getDomNodePagePosition,
  createStyleSheet,
  createCSSRule,
  append,
  $,
  getActiveDocument,
  onDidRegisterWindow,
  getWindows,
} from "vs/base/browser/dom"
import { IConfigurationService } from "vs/platform/configuration/common/configuration"
import {
  ContextKeyExpr,
  IContextKeyService,
  RawContextKey,
} from "vs/platform/contextkey/common/contextkey"
import { Context } from "vs/platform/contextkey/browser/contextKeyService"
import { StandardKeyboardEvent } from "vs/base/browser/keyboardEvent"
import { RunOnceScheduler } from "vs/base/common/async"
import { ILayoutService } from "vs/platform/layout/browser/layoutService"
import { Registry } from "vs/platform/registry/common/platform"
import {
  registerAction2,
  Action2,
  MenuRegistry,
} from "vs/platform/actions/common/actions"
import {
  IStorageService,
  StorageScope,
  StorageTarget,
} from "vs/platform/storage/common/storage"
import { clamp } from "vs/base/common/numbers"
import { KeyCode } from "vs/base/common/keyCodes"
import {
  IConfigurationRegistry,
  Extensions as ConfigurationExtensions,
} from "vs/platform/configuration/common/configurationRegistry"
import { ILogService } from "vs/platform/log/common/log"
import { IWorkingCopyService } from "vs/workbench/services/workingCopy/common/workingCopyService"
import { ServicesAccessor } from "vs/platform/instantiation/common/instantiation"
import { Categories } from "vs/platform/action/common/actionCommonCategories"
import { IWorkingCopyBackupService } from "vs/workbench/services/workingCopy/common/workingCopyBackup"
import {
  ResolutionResult,
  ResultKind,
} from "vs/platform/keybinding/common/keybindingResolver"
import { IDialogService } from "vs/platform/dialogs/common/dialogs"
import { IOutputService } from "vs/workbench/services/output/common/output"
import { windowLogId } from "vs/workbench/services/log/common/logConstants"
import { ByteSize } from "vs/platform/files/common/files"
import {
  IQuickInputService,
  IQuickPickItem,
} from "vs/platform/quickinput/common/quickInput"
import { IUserDataProfileService } from "vs/workbench/services/userDataProfile/common/userDataProfile"
import { IEditorService } from "vs/workbench/services/editor/common/editorService"
import product from "vs/platform/product/common/product"
import { CommandsRegistry } from "vs/platform/commands/common/commands"
import { IEnvironmentService } from "vs/platform/environment/common/environment"

class InspectContextKeysAction extends Action2 {
  constructor() {
    super({
      id: "workbench.action.inspectContextKeys",
      title: localize2("inspect context keys", "Inspect Context Keys"),
      category: Categories.Developer,
      f1: true,
    })
  }

  run(accessor: ServicesAccessor): void {
    const contextKeyService = accessor.get(IContextKeyService)

    const disposables = new DisposableStore()

    const stylesheet = createStyleSheet(undefined, undefined, disposables)
    createCSSRule("*", "cursor: crosshair !important;", stylesheet)

    const hoverFeedback = document.createElement("div")
    const activeDocument = getActiveDocument()
    activeDocument.body.appendChild(hoverFeedback)
    disposables.add(
      toDisposable(() => activeDocument.body.removeChild(hoverFeedback)),
    )

    hoverFeedback.style.position = "absolute"
    hoverFeedback.style.pointerEvents = "none"
    hoverFeedback.style.backgroundColor = "rgba(255, 0, 0, 0.5)"
    hoverFeedback.style.zIndex = "1000"

    const onMouseMove = disposables.add(
      new DomEmitter(activeDocument, "mousemove", true),
    )
    disposables.add(
      onMouseMove.event((e) => {
        const target = e.target as HTMLElement
        const position = getDomNodePagePosition(target)

        hoverFeedback.style.top = `${position.top}px`
        hoverFeedback.style.left = `${position.left}px`
        hoverFeedback.style.width = `${position.width}px`
        hoverFeedback.style.height = `${position.height}px`
      }),
    )

    const onMouseDown = disposables.add(
      new DomEmitter(activeDocument, "mousedown", true),
    )
    Event.once(onMouseDown.event)(
      (e) => {
        e.preventDefault()
        e.stopPropagation()
      },
      null,
      disposables,
    )

    const onMouseUp = disposables.add(
      new DomEmitter(activeDocument, "mouseup", true),
    )
    Event.once(onMouseUp.event)(
      (e) => {
        e.preventDefault()
        e.stopPropagation()

        const context = contextKeyService.getContext(
          e.target as HTMLElement,
        ) as Context
        console.log(context.collectAllValues())

        dispose(disposables)
      },
      null,
      disposables,
    )
  }
}

interface IScreencastKeyboardOptions {
  readonly showKeys?: boolean
  readonly showKeybindings?: boolean
  readonly showCommands?: boolean
  readonly showCommandGroups?: boolean
  readonly showSingleEditorCursorMoves?: boolean
}

class ToggleScreencastModeAction extends Action2 {
  static disposable: IDisposable | undefined

  constructor() {
    super({
      id: "workbench.action.toggleScreencastMode",
      title: localize2("toggle screencast mode", "Toggle Screencast Mode"),
      category: Categories.Developer,
      f1: true,
    })
  }

  run(accessor: ServicesAccessor): void {
    if (ToggleScreencastModeAction.disposable) {
      ToggleScreencastModeAction.disposable.dispose()
      ToggleScreencastModeAction.disposable = undefined
      return
    }

    const layoutService = accessor.get(ILayoutService)
    const configurationService = accessor.get(IConfigurationService)
    const keybindingService = accessor.get(IKeybindingService)

    const disposables = new DisposableStore()

    const container = layoutService.activeContainer

    const mouseMarker = append(container, $(".screencast-mouse"))
    disposables.add(toDisposable(() => mouseMarker.remove()))

    const keyboardMarker = append(container, $(".screencast-keyboard"))
    disposables.add(toDisposable(() => keyboardMarker.remove()))

    const onMouseDown = disposables.add(new Emitter<MouseEvent>())
    const onMouseUp = disposables.add(new Emitter<MouseEvent>())
    const onMouseMove = disposables.add(new Emitter<MouseEvent>())

    function registerContainerListeners(
      container: HTMLElement,
      disposables: DisposableStore,
    ): void {
      disposables.add(
        disposables
          .add(new DomEmitter(container, "mousedown", true))
          .event((e) => onMouseDown.fire(e)),
      )
      disposables.add(
        disposables
          .add(new DomEmitter(container, "mouseup", true))
          .event((e) => onMouseUp.fire(e)),
      )
      disposables.add(
        disposables
          .add(new DomEmitter(container, "mousemove", true))
          .event((e) => onMouseMove.fire(e)),
      )
    }

    for (const { window, disposables } of getWindows()) {
      registerContainerListeners(
        layoutService.getContainer(window),
        disposables,
      )
    }

    disposables.add(
      onDidRegisterWindow(({ window, disposables }) =>
        registerContainerListeners(
          layoutService.getContainer(window),
          disposables,
        ),
      ),
    )

    disposables.add(
      layoutService.onDidChangeActiveContainer(() => {
        layoutService.activeContainer.appendChild(mouseMarker)
        layoutService.activeContainer.appendChild(keyboardMarker)
      }),
    )

    const updateMouseIndicatorColor = () => {
      mouseMarker.style.borderColor = Color.fromHex(
        configurationService.getValue<string>(
          "screencastMode.mouseIndicatorColor",
        ),
      ).toString()
    }

    let mouseIndicatorSize: number
    const updateMouseIndicatorSize = () => {
      mouseIndicatorSize = clamp(
        configurationService.getValue<number>(
          "screencastMode.mouseIndicatorSize",
        ) || 20,
        20,
        100,
      )

      mouseMarker.style.height = `${mouseIndicatorSize}px`
      mouseMarker.style.width = `${mouseIndicatorSize}px`
    }

    updateMouseIndicatorColor()
    updateMouseIndicatorSize()

    disposables.add(
      onMouseDown.event((e) => {
        mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`
        mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`
        mouseMarker.style.display = "block"
        mouseMarker.style.transform = `scale(${1})`
        mouseMarker.style.transition = "transform 0.1s"

        const mouseMoveListener = onMouseMove.event((e) => {
          mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`
          mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`
          mouseMarker.style.transform = `scale(${0.8})`
        })

        Event.once(onMouseUp.event)(() => {
          mouseMarker.style.display = "none"
          mouseMoveListener.dispose()
        })
      }),
    )

    const updateKeyboardFontSize = () => {
      keyboardMarker.style.fontSize = `${clamp(configurationService.getValue<number>("screencastMode.fontSize") || 56, 20, 100)}px`
    }

    const updateKeyboardMarker = () => {
      keyboardMarker.style.bottom = `${clamp(configurationService.getValue<number>("screencastMode.verticalOffset") || 0, 0, 90)}%`
    }

    let keyboardMarkerTimeout!: number
    const updateKeyboardMarkerTimeout = () => {
      keyboardMarkerTimeout = clamp(
        configurationService.getValue<number>(
          "screencastMode.keyboardOverlayTimeout",
        ) || 800,
        500,
        5000,
      )
    }

    updateKeyboardFontSize()
    updateKeyboardMarker()
    updateKeyboardMarkerTimeout()

    disposables.add(
      configurationService.onDidChangeConfiguration((e) => {
        if (e.affectsConfiguration("screencastMode.verticalOffset")) {
          updateKeyboardMarker()
        }

        if (e.affectsConfiguration("screencastMode.fontSize")) {
          updateKeyboardFontSize()
        }

        if (e.affectsConfiguration("screencastMode.keyboardOverlayTimeout")) {
          updateKeyboardMarkerTimeout()
        }

        if (e.affectsConfiguration("screencastMode.mouseIndicatorColor")) {
          updateMouseIndicatorColor()
        }

        if (e.affectsConfiguration("screencastMode.mouseIndicatorSize")) {
          updateMouseIndicatorSize()
        }
      }),
    )

    const onKeyDown = disposables.add(new Emitter<KeyboardEvent>())
    const onCompositionStart = disposables.add(new Emitter<CompositionEvent>())
    const onCompositionUpdate = disposables.add(new Emitter<CompositionEvent>())
    const onCompositionEnd = disposables.add(new Emitter<CompositionEvent>())

    function registerWindowListeners(
      window: Window,
      disposables: DisposableStore,
    ): void {
      disposables.add(
        disposables
          .add(new DomEmitter(window, "keydown", true))
          .event((e) => onKeyDown.fire(e)),
      )
      disposables.add(
        disposables
          .add(new DomEmitter(window, "compositionstart", true))
          .event((e) => onCompositionStart.fire(e)),
      )
      disposables.add(
        disposables
          .add(new DomEmitter(window, "compositionupdate", true))
          .event((e) => onCompositionUpdate.fire(e)),
      )
      disposables.add(
        disposables
          .add(new DomEmitter(window, "compositionend", true))
          .event((e) => onCompositionEnd.fire(e)),
      )
    }

    for (const { window, disposables } of getWindows()) {
      registerWindowListeners(window, disposables)
    }

    disposables.add(
      onDidRegisterWindow(({ window, disposables }) =>
        registerWindowListeners(window, disposables),
      ),
    )

    let length = 0
    let composing: Element | undefined = undefined
    let imeBackSpace = false

    const clearKeyboardScheduler = new RunOnceScheduler(() => {
      keyboardMarker.textContent = ""
      composing = undefined
      length = 0
    }, keyboardMarkerTimeout)

    disposables.add(
      onCompositionStart.event((e) => {
        imeBackSpace = true
      }),
    )

    disposables.add(
      onCompositionUpdate.event((e) => {
        if (e.data && imeBackSpace) {
          if (length > 20) {
            keyboardMarker.innerText = ""
            length = 0
          }
          composing = composing ?? append(keyboardMarker, $("span.key"))
          composing.textContent = e.data
        } else if (imeBackSpace) {
          keyboardMarker.innerText = ""
          append(keyboardMarker, $("span.key", {}, `Backspace`))
        }
        clearKeyboardScheduler.schedule()
      }),
    )

    disposables.add(
      onCompositionEnd.event((e) => {
        composing = undefined
        length++
      }),
    )

    disposables.add(
      onKeyDown.event((e) => {
        if (
          e.key === "Process" ||
          /[\uac00-\ud787\u3131-\u314e\u314f-\u3163\u3041-\u3094\u30a1-\u30f4\u30fc\u3005\u3006\u3024\u4e00-\u9fa5]/u.test(
            e.key,
          )
        ) {
          if (e.code === "Backspace") {
            imeBackSpace = true
          } else if (!e.code.includes("Key")) {
            composing = undefined
            imeBackSpace = false
          } else {
            imeBackSpace = true
          }
          clearKeyboardScheduler.schedule()
          return
        }

        if (e.isComposing) {
          return
        }

        const options =
          configurationService.getValue<IScreencastKeyboardOptions>(
            "screencastMode.keyboardOptions",
          )
        const event = new StandardKeyboardEvent(e)
        const shortcut = keybindingService.softDispatch(event, event.target)

        // Hide the single arrow key pressed
        if (
          shortcut.kind === ResultKind.KbFound &&
          shortcut.commandId &&
          !(options.showSingleEditorCursorMoves ?? true) &&
          ["cursorLeft", "cursorRight", "cursorUp", "cursorDown"].includes(
            shortcut.commandId,
          )
        ) {
          return
        }

        if (
          event.ctrlKey ||
          event.altKey ||
          event.metaKey ||
          event.shiftKey ||
          length > 20 ||
          event.keyCode === KeyCode.Backspace ||
          event.keyCode === KeyCode.Escape ||
          event.keyCode === KeyCode.UpArrow ||
          event.keyCode === KeyCode.DownArrow ||
          event.keyCode === KeyCode.LeftArrow ||
          event.keyCode === KeyCode.RightArrow
        ) {
          keyboardMarker.innerText = ""
          length = 0
        }

        const keybinding = keybindingService.resolveKeyboardEvent(event)
        const commandDetails =
          this._isKbFound(shortcut) && shortcut.commandId
            ? this.getCommandDetails(shortcut.commandId)
            : undefined

        let commandAndGroupLabel = commandDetails?.title
        let keyLabel: string | undefined | null = keybinding.getLabel()

        if (commandDetails) {
          if ((options.showCommandGroups ?? false) && commandDetails.category) {
            commandAndGroupLabel = `${commandDetails.category}: ${commandAndGroupLabel} `
          }

          if (this._isKbFound(shortcut) && shortcut.commandId) {
            const keybindings = keybindingService
              .lookupKeybindings(shortcut.commandId)
              .filter((k) => k.getLabel()?.endsWith(keyLabel ?? ""))

            if (keybindings.length > 0) {
              keyLabel = keybindings[keybindings.length - 1].getLabel()
            }
          }
        }

        if ((options.showCommands ?? true) && commandAndGroupLabel) {
          append(
            keyboardMarker,
            $("span.title", {}, `${commandAndGroupLabel} `),
          )
        }

        if (
          (options.showKeys ?? true) ||
          ((options.showKeybindings ?? true) && this._isKbFound(shortcut))
        ) {
          // Fix label for arrow keys
          keyLabel = keyLabel
            ?.replace("UpArrow", "↑")
            ?.replace("DownArrow", "↓")
            ?.replace("LeftArrow", "←")
            ?.replace("RightArrow", "→")

          append(keyboardMarker, $("span.key", {}, keyLabel ?? ""))
        }

        length++
        clearKeyboardScheduler.schedule()
      }),
    )

    ToggleScreencastModeAction.disposable = disposables
  }

  private _isKbFound(resolutionResult: ResolutionResult): resolutionResult is {
    kind: ResultKind.KbFound
    commandId: string | null
    commandArgs: any
    isBubble: boolean
  } {
    return resolutionResult.kind === ResultKind.KbFound
  }

  private getCommandDetails(
    commandId: string,
  ): { title: string; category?: string } | undefined {
    const fromMenuRegistry = MenuRegistry.getCommand(commandId)

    if (fromMenuRegistry) {
      return {
        title:
          typeof fromMenuRegistry.title === "string"
            ? fromMenuRegistry.title
            : fromMenuRegistry.title.value,
        category: fromMenuRegistry.category
          ? typeof fromMenuRegistry.category === "string"
            ? fromMenuRegistry.category
            : fromMenuRegistry.category.value
          : undefined,
      }
    }

    const fromCommandsRegistry = CommandsRegistry.getCommand(commandId)

    if (fromCommandsRegistry && fromCommandsRegistry.metadata?.description) {
      return {
        title:
          typeof fromCommandsRegistry.metadata.description === "string"
            ? fromCommandsRegistry.metadata.description
            : fromCommandsRegistry.metadata.description.value,
      }
    }

    return undefined
  }
}

class LogStorageAction extends Action2 {
  constructor() {
    super({
      id: "workbench.action.logStorage",
      title: localize2(
        {
          key: "logStorage",
          comment: [
            "A developer only action to log the contents of the storage for the current window.",
          ],
        },
        "Log Storage Database Contents",
      ),
      category: Categories.Developer,
      f1: true,
    })
  }

  run(accessor: ServicesAccessor): void {
    const storageService = accessor.get(IStorageService)
    const dialogService = accessor.get(IDialogService)

    storageService.log()

    dialogService.info(
      localize(
        "storageLogDialogMessage",
        "The storage database contents have been logged to the developer tools.",
      ),
      localize(
        "storageLogDialogDetails",
        "Open developer tools from the menu and select the Console tab.",
      ),
    )
  }
}

class LogWorkingCopiesAction extends Action2 {
  constructor() {
    super({
      id: "workbench.action.logWorkingCopies",
      title: localize2(
        {
          key: "logWorkingCopies",
          comment: [
            "A developer only action to log the working copies that exist.",
          ],
        },
        "Log Working Copies",
      ),
      category: Categories.Developer,
      f1: true,
    })
  }

  async run(accessor: ServicesAccessor): Promise<void> {
    const workingCopyService = accessor.get(IWorkingCopyService)
    const workingCopyBackupService = accessor.get(IWorkingCopyBackupService)
    const logService = accessor.get(ILogService)
    const outputService = accessor.get(IOutputService)

    const backups = await workingCopyBackupService.getBackups()

    const msg = [
      ``,
      `[Working Copies]`,
      ...(workingCopyService.workingCopies.length > 0
        ? workingCopyService.workingCopies.map(
            (workingCopy) =>
              `${workingCopy.isDirty() ? "● " : ""}${workingCopy.resource.toString(true)} (typeId: ${workingCopy.typeId || "<no typeId>"})`,
          )
        : ["<none>"]),
      ``,
      `[Backups]`,
      ...(backups.length > 0
        ? backups.map(
            (backup) =>
              `${backup.resource.toString(true)} (typeId: ${backup.typeId || "<no typeId>"})`,
          )
        : ["<none>"]),
    ]

    logService.info(msg.join("\n"))

    outputService.showChannel(windowLogId, true)
  }
}

class RemoveLargeStorageEntriesAction extends Action2 {
  private static SIZE_THRESHOLD = 1024 * 16 // 16kb

  constructor() {
    super({
      id: "workbench.action.removeLargeStorageDatabaseEntries",
      title: localize2(
        "removeLargeStorageDatabaseEntries",
        "Remove Large Storage Database Entries...",
      ),
      category: Categories.Developer,
      f1: true,
    })
  }

  async run(accessor: ServicesAccessor): Promise<void> {
    const storageService = accessor.get(IStorageService)
    const quickInputService = accessor.get(IQuickInputService)
    const userDataProfileService = accessor.get(IUserDataProfileService)
    const dialogService = accessor.get(IDialogService)
    const environmentService = accessor.get(IEnvironmentService)

    interface IStorageItem extends IQuickPickItem {
      readonly key: string
      readonly scope: StorageScope
      readonly target: StorageTarget
      readonly size: number
    }

    const items: IStorageItem[] = []

    for (const scope of [
      StorageScope.APPLICATION,
      StorageScope.PROFILE,
      StorageScope.WORKSPACE,
    ]) {
      if (
        scope === StorageScope.PROFILE &&
        userDataProfileService.currentProfile.isDefault
      ) {
        continue // avoid duplicates
      }

      for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
        for (const key of storageService.keys(scope, target)) {
          const value = storageService.get(key, scope)
          if (
            value &&
            (!environmentService.isBuilt /* show all keys in dev */ ||
              value.length > RemoveLargeStorageEntriesAction.SIZE_THRESHOLD)
          ) {
            items.push({
              key,
              scope,
              target,
              size: value.length,
              label: key,
              description: ByteSize.formatSize(value.length),
              detail: localize(
                "largeStorageItemDetail",
                "Scope: {0}, Target: {1}",
                scope === StorageScope.APPLICATION
                  ? localize("global", "Global")
                  : scope === StorageScope.PROFILE
                    ? localize("profile", "Profile")
                    : localize("workspace", "Workspace"),
                target === StorageTarget.MACHINE
                  ? localize("machine", "Machine")
                  : localize("user", "User"),
              ),
            })
          }
        }
      }
    }

    items.sort((itemA, itemB) => itemB.size - itemA.size)

    const selectedItems = await new Promise<readonly IStorageItem[]>(
      (resolve) => {
        const disposables = new DisposableStore()

        const picker = disposables.add(
          quickInputService.createQuickPick<IStorageItem>(),
        )
        picker.items = items
        picker.canSelectMany = true
        picker.ok = false
        picker.customButton = true
        picker.hideCheckAll = true
        picker.customLabel = localize(
          "removeLargeStorageEntriesPickerButton",
          "Remove",
        )
        picker.placeholder = localize(
          "removeLargeStorageEntriesPickerPlaceholder",
          "Select large entries to remove from storage",
        )

        if (items.length === 0) {
          picker.description = localize(
            "removeLargeStorageEntriesPickerDescriptionNoEntries",
            "There are no large storage entries to remove.",
          )
        }

        picker.show()

        disposables.add(
          picker.onDidCustom(() => {
            resolve(picker.selectedItems)
            picker.hide()
          }),
        )

        disposables.add(picker.onDidHide(() => disposables.dispose()))
      },
    )

    if (selectedItems.length === 0) {
      return
    }

    const { confirmed } = await dialogService.confirm({
      type: "warning",
      message: localize(
        "removeLargeStorageEntriesConfirmRemove",
        "Do you want to remove the selected storage entries from the database?",
      ),
      detail: localize(
        "removeLargeStorageEntriesConfirmRemoveDetail",
        "{0}\n\nThis action is irreversible and may result in data loss!",
        selectedItems.map((item) => item.label).join("\n"),
      ),
      primaryButton: localize(
        {
          key: "removeLargeStorageEntriesButtonLabel",
          comment: ["&& denotes a mnemonic"],
        },
        "&&Remove",
      ),
    })

    if (!confirmed) {
      return
    }

    const scopesToOptimize = new Set<StorageScope>()
    for (const item of selectedItems) {
      storageService.remove(item.key, item.scope)
      scopesToOptimize.add(item.scope)
    }

    for (const scope of scopesToOptimize) {
      await storageService.optimize(scope)
    }
  }
}

let tracker: DisposableTracker | undefined = undefined
let trackedDisposables = new Set<IDisposable>()

const DisposablesSnapshotStateContext = new RawContextKey<
  "started" | "pending" | "stopped"
>("dirtyWorkingCopies", "stopped")

class StartTrackDisposables extends Action2 {
  constructor() {
    super({
      id: "workbench.action.startTrackDisposables",
      title: localize2("startTrackDisposables", "Start Tracking Disposables"),
      category: Categories.Developer,
      f1: true,
      precondition: ContextKeyExpr.and(
        DisposablesSnapshotStateContext.isEqualTo("pending").negate(),
        DisposablesSnapshotStateContext.isEqualTo("started").negate(),
      ),
    })
  }

  run(accessor: ServicesAccessor): void {
    const disposablesSnapshotStateContext =
      DisposablesSnapshotStateContext.bindTo(accessor.get(IContextKeyService))
    disposablesSnapshotStateContext.set("started")

    trackedDisposables.clear()

    tracker = new DisposableTracker()
    setDisposableTracker(tracker)
  }
}

class SnapshotTrackedDisposables extends Action2 {
  constructor() {
    super({
      id: "workbench.action.snapshotTrackedDisposables",
      title: localize2(
        "snapshotTrackedDisposables",
        "Snapshot Tracked Disposables",
      ),
      category: Categories.Developer,
      f1: true,
      precondition: DisposablesSnapshotStateContext.isEqualTo("started"),
    })
  }

  run(accessor: ServicesAccessor): void {
    const disposablesSnapshotStateContext =
      DisposablesSnapshotStateContext.bindTo(accessor.get(IContextKeyService))
    disposablesSnapshotStateContext.set("pending")

    trackedDisposables = new Set(
      tracker
        ?.computeLeakingDisposables(1000)
        ?.leaks.map((disposable) => disposable.value),
    )
  }
}

class StopTrackDisposables extends Action2 {
  constructor() {
    super({
      id: "workbench.action.stopTrackDisposables",
      title: localize2("stopTrackDisposables", "Stop Tracking Disposables"),
      category: Categories.Developer,
      f1: true,
      precondition: DisposablesSnapshotStateContext.isEqualTo("pending"),
    })
  }

  run(accessor: ServicesAccessor): void {
    const editorService = accessor.get(IEditorService)

    const disposablesSnapshotStateContext =
      DisposablesSnapshotStateContext.bindTo(accessor.get(IContextKeyService))
    disposablesSnapshotStateContext.set("stopped")

    if (tracker) {
      const disposableLeaks = new Set<DisposableInfo>()

      for (const disposable of new Set(
        tracker.computeLeakingDisposables(1000)?.leaks,
      ) ?? []) {
        if (trackedDisposables.has(disposable.value)) {
          disposableLeaks.add(disposable)
        }
      }

      const leaks = tracker.computeLeakingDisposables(
        1000,
        Array.from(disposableLeaks),
      )
      if (leaks) {
        editorService.openEditor({
          resource: undefined,
          contents: leaks.details,
        })
      }
    }

    setDisposableTracker(null)
    tracker = undefined
    trackedDisposables.clear()
  }
}

// --- Actions Registration
registerAction2(InspectContextKeysAction)
registerAction2(ToggleScreencastModeAction)
registerAction2(LogStorageAction)
registerAction2(LogWorkingCopiesAction)
registerAction2(RemoveLargeStorageEntriesAction)
if (!product.commit) {
  registerAction2(StartTrackDisposables)
  registerAction2(SnapshotTrackedDisposables)
  registerAction2(StopTrackDisposables)
}

// --- Configuration

// Screen Cast Mode
const configurationRegistry = Registry.as<IConfigurationRegistry>(
  ConfigurationExtensions.Configuration,
)
configurationRegistry.registerConfiguration({
  id: "screencastMode",
  order: 9,
  title: localize("screencastModeConfigurationTitle", "Screencast Mode"),
  type: "object",
  properties: {
    "screencastMode.verticalOffset": {
      type: "number",
      default: 20,
      minimum: 0,
      maximum: 90,
      description: localize(
        "screencastMode.location.verticalPosition",
        "Controls the vertical offset of the screencast mode overlay from the bottom as a percentage of the workbench height.",
      ),
    },
    "screencastMode.fontSize": {
      type: "number",
      default: 56,
      minimum: 20,
      maximum: 100,
      description: localize(
        "screencastMode.fontSize",
        "Controls the font size (in pixels) of the screencast mode keyboard.",
      ),
    },
    "screencastMode.keyboardOptions": {
      type: "object",
      description: localize(
        "screencastMode.keyboardOptions.description",
        "Options for customizing the keyboard overlay in screencast mode.",
      ),
      properties: {
        showKeys: {
          type: "boolean",
          default: true,
          description: localize(
            "screencastMode.keyboardOptions.showKeys",
            "Show raw keys.",
          ),
        },
        showKeybindings: {
          type: "boolean",
          default: true,
          description: localize(
            "screencastMode.keyboardOptions.showKeybindings",
            "Show keyboard shortcuts.",
          ),
        },
        showCommands: {
          type: "boolean",
          default: true,
          description: localize(
            "screencastMode.keyboardOptions.showCommands",
            "Show command names.",
          ),
        },
        showCommandGroups: {
          type: "boolean",
          default: false,
          description: localize(
            "screencastMode.keyboardOptions.showCommandGroups",
            "Show command group names, when commands are also shown.",
          ),
        },
        showSingleEditorCursorMoves: {
          type: "boolean",
          default: true,
          description: localize(
            "screencastMode.keyboardOptions.showSingleEditorCursorMoves",
            "Show single editor cursor move commands.",
          ),
        },
      },
      default: {
        showKeys: true,
        showKeybindings: true,
        showCommands: true,
        showCommandGroups: false,
        showSingleEditorCursorMoves: true,
      },
      additionalProperties: false,
    },
    "screencastMode.keyboardOverlayTimeout": {
      type: "number",
      default: 800,
      minimum: 500,
      maximum: 5000,
      description: localize(
        "screencastMode.keyboardOverlayTimeout",
        "Controls how long (in milliseconds) the keyboard overlay is shown in screencast mode.",
      ),
    },
    "screencastMode.mouseIndicatorColor": {
      type: "string",
      format: "color-hex",
      default: "#FF0000",
      description: localize(
        "screencastMode.mouseIndicatorColor",
        "Controls the color in hex (#RGB, #RGBA, #RRGGBB or #RRGGBBAA) of the mouse indicator in screencast mode.",
      ),
    },
    "screencastMode.mouseIndicatorSize": {
      type: "number",
      default: 20,
      minimum: 20,
      maximum: 100,
      description: localize(
        "screencastMode.mouseIndicatorSize",
        "Controls the size (in pixels) of the mouse indicator in screencast mode.",
      ),
    },
  },
})
